﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace VoxelConverter.VoxFormats
{
    public class MagicaVoxel
    {

        public static VoxelModel Load(string file)
        {
            FileStream fs = File.Open(file, FileMode.Open);

            byte[] buffer = new byte[4];

            // Read Header Magic Number
            fs.Read(buffer, 0, 4);
            if (StringFromBuffer(buffer) != "VOX ")
                return null;

            // Read Version Number, Discard
            fs.Read(buffer, 0, 4);

            // Read MAIN Chunk
            MVoxChunk main = ReadMVoxChunk(fs);

            return LoadFromMVoxChunk(main);
        }

        private static VoxelModel LoadFromMVoxChunk(MVoxChunk main)
        {
            // Get the SIZE chunk
            MVoxChunk size = main.Children.SingleOrDefault(c => c.ID.Contains("SIZE"));

            // Get VOXEL chunk
            MVoxChunk voxels = main.Children.SingleOrDefault(c => c.ID.Contains("XYZI"));

            if (size != null && voxels != null)
            {
                UInt32 _x, _y, _z;
                _x = ConvertToUInt32(size.Contents.SubArray(0, 4), 0);
                _y = ConvertToUInt32(size.Contents.SubArray(4, 4), 0);
                _z = ConvertToUInt32(size.Contents.SubArray(8, 4), 0);

                if (_x > 0 && _y > 0 && _z > 0)
                {
                    VoxelModel model = new VoxelModel((int)_x, (int)_y, (int)_z);
                    List<int> vIndices = new List<int>();

                    // Read Number of voxels
                    UInt32 numVoxels = ConvertToUInt32(voxels.Contents.SubArray(0, 4), 0);
                    int offset = 4;

                    for (int i = 0; i < numVoxels; i++)
                    {
                        byte vX, vY, vZ, vI;
                        vX = voxels.Contents[(i * 4) + offset];
                        vY = voxels.Contents[(i * 4) + offset + 1];
                        vZ = voxels.Contents[(i * 4) + offset + 2];
                        vI = voxels.Contents[(i * 4) + offset + 3];

                        int index = vX + (vY * model.Width) + (vZ * model.Width * model.Height);
                        if (index < _x * _y * _z)
                        {
                            model.VoxelData[index] = (int)vI;
                            if (vIndices.Count > 0 && model.VoxelData[index] >= 0)
                            {
                                if (!vIndices.Contains(model.VoxelData[index]))
                                    vIndices.Add(model.VoxelData[index]);
                            }
                            else if (model.VoxelData[index] >= 0)
                            {
                                vIndices.Add(model.VoxelData[index]);
                            }
                        }
                    }

                    model.VoxelIndices = vIndices.ToArray();

                    return model;
                }
            }

            Console.WriteLine("SIZE or XYZI was null");

            return null;
        }

        private static string StringFromBuffer(byte[] buffer)
        {
            if (buffer == null)
                return string.Empty;

            string value = string.Empty;

            for (int i = 0; i < buffer.Length; i++)
            {
                value += (char)buffer[i];
            }

            return value;
        }

        private static MVoxChunk ReadMVoxChunk(Stream stream)
        {
            MVoxChunk chunk = new MVoxChunk()
            {
                Size = 0,
                ChildrenSize = 0,
                ID = string.Empty,
                Contents = null
            };

            byte[] buffer = new byte[4];

            // Read ID
            if (stream.Read(buffer, 0, 4) > 0)
                chunk.ID = StringFromBuffer(buffer);

            // Read Size
            stream.Read(buffer, 0, 4);
            chunk.Size = ConvertToUInt32(buffer, 0);

            // Read Size of Children
            if (stream.Read(buffer, 0, 4) > 0)
                chunk.ChildrenSize = ConvertToUInt32(buffer, 0);

            // Read Data
            if (chunk.Size > 0)
            {
                chunk.Contents = new byte[chunk.Size];
                byte[] _tmp = new byte[1];
                for (UInt32 i = 0; i < chunk.Size; i++)
                {
                    if (stream.Read(_tmp, 0, 1) > 0)
                        chunk.Contents[i] = _tmp[0];
                    else
                        chunk.Contents[i] = 0x00;
                }
            }

            // Read Children Chunks
            if (chunk.ChildrenSize > 0)
            {
                chunk.Children = new List<MVoxChunk>();

                // Recursively Read the Children chunks.
                byte[] childBuffer = new byte[chunk.ChildrenSize];
                if (stream.Read(childBuffer, 0, (int)chunk.ChildrenSize) > 0)
                {
                    MemoryStream _mStream = new MemoryStream(childBuffer);
                    while (_mStream.Position < _mStream.Length)
                    {
                        chunk.Children.Add(ReadMVoxChunk(_mStream));
                    }
                }
            }

            return chunk;
        }

        private static UInt32 ConvertToUInt32(byte[] buffer, int offset, bool swap = false)
        {
            if (buffer != null)
            {
                if (!swap)
                {
                    return BitConverter.ToUInt32(buffer, offset);
                }
                else
                {
                    // XOR Swapping for endianness
                    for (int i = 0; i < buffer.Length; i += 2)
                    {
                        buffer[i] ^= buffer[i + 1];
                        buffer[i + 1] ^= buffer[i];
                        buffer[i] ^= buffer[i + 1];
                    }

                    return BitConverter.ToUInt32(buffer, offset);
                }
            }
            else
                return (UInt32)0;
        }

        private class MVoxChunk
        {
            public string ID { get; set; }
            public UInt32 Size { get; set; }
            public UInt32 ChildrenSize { get; set; }
            public byte[] Contents { get; set; }
            public List<MVoxChunk> Children { get; set; }
        }
    }
}
